Skip to content

feat: add async support (AsyncMockFirestore) ported from mdowds#62#5

Open
Some1Somewhere wants to merge 1 commit into
masterfrom
feat/async-support
Open

feat: add async support (AsyncMockFirestore) ported from mdowds#62#5
Some1Somewhere wants to merge 1 commit into
masterfrom
feat/async-support

Conversation

@Some1Somewhere

Copy link
Copy Markdown
Owner

What

Adds async support (AsyncMockFirestore + async reference classes) to this fork, ported from upstream mdowds/python-mock-firestore#62 (credit: @anna-hope), which was never merged upstream.

Ported (additive only):

  • 5 new modules: async_client.py, async_collection.py, async_document.py, async_query.py, async_transaction.py
  • consume_async_iterable helper added to _helpers.py
  • Async exports added to __init__.py (AsyncMockFirestore, AsyncDocumentReference, AsyncCollectionReference, AsyncQuery, AsyncTransaction)
  • 5 async test files (203 → 207 tests; aiounittest added to requirements-dev-minimal.txt)

What was deliberately NOT taken from PR mdowds#62

PR mdowds#62 also refactored the sync classes (client.py, collection.py, query.py, transaction.py, _helpers.py) and black-reformatted the whole package. Both were skipped: this fork has diverged on exactly those files with semantic fixes (DELETE_FIELD-doesn't-fail, don't-drop-empty-dicts, set(merge=True), collection.get generator fix, query iteration) that a wholesale merge would clobber.

Adaptations needed to fit this fork's sync API

The async layer was written against PR mdowds#62's refactored sync classes, so four surgical adaptations were required:

  1. async_query.py called Query._process_field_filters / _process_pagination, which only exist in PR Add async support and format with black mdowds/python-mock-firestore#62's refactor. Instead, the filtering/ordering/cursor/limit body of this fork's Query.stream was extracted into Query._process_snapshots (pure extract-method; sync behavior unchanged) and the async query reuses it.
  2. async_collection.py::get inherited list(self.stream()), which breaks when stream is an async generator — now await consume_async_iterable(self.stream()). Its where() also gained this fork's filter=FieldFilter keyword support to match the sync signature.
  3. async_document.py::set re-implemented PR Add async support and format with black mdowds/python-mock-firestore#62-era merge logic, which would bypass this fork's set(merge=True) fix and break with unawaited coroutines (the sync set calls self.update, which dispatches to the async override). It now mirrors this fork's merge branch (flatten_for_merge + __name__ stamping) with proper awaits.
  4. client.py::_ensure_path used exact type() in (...) checks, which misroute AsyncMockFirestore/AsyncDocumentReference when resolving multi-segment paths. Changed to isinstance — behavior-identical for the sync classes (CollectionReference is not a subclass of either type).

AsyncMockFirestore shares the sync in-memory store

Yes — verified empirically. AsyncMockFirestore subclasses MockFirestore; all reads/writes (sync and async reference classes alike) operate on the same plain-dict _data store. Consequences for consumers:

  • A sync MockFirestore view pointed at the same store lets test fixtures keep seeding synchronously while production code under test reads via await:
    afs = AsyncMockFirestore()
    sync_view = MockFirestore()
    sync_view._data = afs._data
    sync_view.collection('users').document('u1').set({'name': 'Ada'})   # sync seed
    await afs.collection('users').document('u1').get()                   # sees it
  • Async writes are equally visible to sync reads (verified both directions). No shared-store shim is needed for the future editor/scraper async migration.

Test results

Suite Result
This fork (pytest, sync + async) 205 passed, 2 failed — both failures are the same pre-existing ArrayUnion-on-nested-dot-notation bug: test_document_update_nestedFieldDotNotationMultipleNestedWithTransformer fails identically on master (verified: 1 failed, 36 passed there), and the async file carries a verbatim twin of that test. Not introduced by this PR.
TrueMark editor (pytest) 178 passed, 1 failed — the known pre-existing tests/test_attachment_manager.py::test_upload_attachments_real (local playwright Chromium binary missing).
TrueMark scraper (pytest -rA) 1900 passed, 11 skipped, 0 failed (venv verified to resolve to this branch).

Sync test files are untouched, and the sync suite result is identical to master — the port is additive.

🤖 Generated with Claude Code

Ported from mdowds/python-mock-firestore PR mdowds#62 (author: anna-hope),
taking only the additive async layer and adapting it to this fork's
sync API instead of PR mdowds#62's refactored one:

- async_query uses Query._process_snapshots (extracted from
  Query.stream, behavior unchanged) instead of PR mdowds#62's
  _process_field_filters/_process_pagination
- async_collection.get consumes its async stream instead of
  inheriting the sync list(stream) implementation; where() supports
  FieldFilter like this fork's sync where()
- async_document.set mirrors this fork's set(merge=True) semantics
  (flatten_for_merge, __name__ stamping) with proper awaits
- MockFirestore._ensure_path uses isinstance so async subclasses
  route document/collection paths correctly
- consume_async_iterable helper added to _helpers
Copilot AI review requested due to automatic review settings June 10, 2026 19:53

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR ports upstream async Firestore-mock support into this fork by adding AsyncMockFirestore plus async variants of collection/document/query/transaction, while keeping the existing sync implementation and storage model intact.

Changes:

  • Added async client + reference/query/transaction modules and exported them from mockfirestore.__init__.
  • Added consume_async_iterable helper to support consuming async generators in API and tests.
  • Introduced a new async test suite and added aiounittest to minimal dev requirements.

Reviewed changes

Copilot reviewed 15 out of 15 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
mockfirestore/async_client.py Adds AsyncMockFirestore async client wrapper around the existing in-memory store.
mockfirestore/async_collection.py Adds async collection reference with async streaming/get and query builders.
mockfirestore/async_document.py Adds async document reference methods (get/set/update/delete) and async subcollection creation.
mockfirestore/async_query.py Adds async query streaming/get using the shared snapshot processing logic.
mockfirestore/async_transaction.py Adds async transaction commit and async iterables for reads.
mockfirestore/_helpers.py Adds consume_async_iterable helper and extends typing imports.
mockfirestore/__init__.py Exports the new async public API types.
mockfirestore/query.py Extracts shared snapshot processing into _process_snapshots for reuse by async queries.
mockfirestore/client.py Adjusts _ensure_path to use isinstance for correct async-path resolution.
requirements-dev-minimal.txt Adds aiounittest for async tests.
tests/test_async_collection_reference.py New async tests for collection reference behaviors (stream/query/cursors/etc).
tests/test_async_document_reference.py New async tests for document reference behaviors (get/set/update/delete/transforms).
tests/test_async_mock_client.py New async tests for client-level APIs (get_all, collections).
tests/test_async_query.py New async tests for query .get() behavior.
tests/test_async_transaction.py New async tests for transaction read/write operations.
Comments suppressed due to low confidence (1)

mockfirestore/async_transaction.py:45

  • AsyncTransaction relies on inheriting the synchronous Transaction.commit() method, which returns a coroutine here (because _commit is async). That works if callers always remember to await, but it is an easy footgun and makes the async API surface ambiguous. Also, __aexit__ currently leaves the transaction in-progress with pending write ops when an exception occurs in the context block; cleaning up on error prevents state leakage.
    async def __aenter__(self):
        return self

    async def __aexit__(self, exc_type, exc_val, exc_tb):
        if exc_type is None:
            await self.commit()


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +11 to +14
def document(self, path: str) -> AsyncDocumentReference:
doc = super().document(path)
assert isinstance(doc, AsyncDocumentReference)
return doc
Comment on lines +54 to +56
def limit(self, limit_amount: int) -> AsyncQuery:
query = AsyncQuery(self, limit=limit_amount)
return query
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants